using System;
using System.Collections.Generic;

namespace Framework
{
    public interface IObjectPool { }

    public interface IObjectPool<T> : IObjectPool where T : class
    {
        public T Get();
        public void Release(T obj);
    }
    public sealed class ObjectPool<T> : IObjectPool where T : class
    {
        private sealed class PooledObject
        {
            public PooledObject(T value) => Value = value;
            public readonly T Value;
            public bool Destroyed { get; private set; } = false;
            public void SetDestroyed() => Destroyed = true;
        }

        private readonly Stack<PooledObject> idleObjects = new();
        private readonly HashSet<PooledObject> activeObjects = new();
        private readonly Dictionary<T, PooledObject> allObjects = new();

        public int IdleCount => idleObjects.Count;
        public int ActiveCount => activeObjects.Count;
        public int TotalCount => allObjects.Count;

        private readonly Func<T> Factory;
        private readonly Action<T> OnActivateAction;
        private readonly Action<T> OnReleaseAction;
        private readonly Action<T> OnHandledAction;
        private readonly Action<T> OnDestroyAction;

        public int MaxSize;
        public bool IsFull => MaxSize > 0 && allObjects.Count >= MaxSize;

        public ObjectPool(Func<T> factory, Action<T> OnActivate = null, Action<T> OnRelease = null,
            Action<T> OnHandled = null, Action<T> OnDestroy = null, int maxSize = -1)
        {
            Factory = factory ?? throw new ArgumentNullException(nameof(factory));
            OnActivateAction = OnActivate;
            OnReleaseAction = OnRelease;
            OnHandledAction = OnHandled;
            OnDestroyAction = OnDestroy;
            MaxSize = maxSize;
        }

        private PooledObject Handle(T obj)
        {
            OnHandledAction?.Invoke(obj);
            var pooled = new PooledObject(obj);
            allObjects.Add(obj, pooled);
            return pooled;
        }

        private PooledObject Create()
        {
            var obj = Factory.Invoke();
            if (obj == null) throw new Exception("Factory returned null");
            if (allObjects.ContainsKey(obj)) new Exception("Factory returned duplicated object");
            var pooled = Handle(obj);
            return pooled;
        }

        private void Destroy(T obj)
        {
            OnDestroyAction?.Invoke(obj);
            if (allObjects.Remove(obj, out var pooled))
                pooled.SetDestroyed();
        }

        private T Activate(PooledObject pooled)
        {
            if (activeObjects.Add(pooled))
                OnActivateAction?.Invoke(pooled.Value);
            return pooled.Value;
        }

        public T Get()
        {
            while (idleObjects.TryPop(out var pooled))
            {
                if (pooled.Destroyed || pooled.Value == null)
                {
                    if (pooled.Value is not null) allObjects.Remove(pooled.Value);
                    continue;
                }
                return Activate(pooled);
            }

            return Activate(Create());
        }

        public void Release(T obj)
        {
            if (IsFull)
            {
                Destroy(obj);
                return;
            }

            if (!allObjects.TryGetValue(obj, out var pooled))
                pooled = Handle(obj);
            activeObjects.Remove(pooled);
            idleObjects.Push(pooled);
            OnReleaseAction?.Invoke(obj);
        }

        public void Clear(int keep)
        {
            while (idleObjects.Count > keep)
            {
                var pooled = idleObjects.Pop();
                allObjects.Remove(pooled.Value);
                Destroy(pooled.Value);
            }
        }

        public void ForceClear()
        {
            var allObjectsCopy = new List<PooledObject>(allObjects.Values);
            idleObjects.Clear();
            activeObjects.Clear();
            allObjects.Clear();
            foreach (var pooled in allObjectsCopy)
            {
                if (!pooled.Destroyed)
                {
                    OnDestroyAction?.Invoke(pooled.Value);
                    pooled.SetDestroyed();
                }
            }
        }
    }
}